iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Modern Web

微光採集瓶:用 Vue 3 打造每日美好記錄網頁系列 第 21 篇

【Day 21】打造時光相簿:將月度記憶留存為小圖卡

  • 分享至 

  • xImage
  •  

昨天我們把月度色卡完成得差不多了:算出了各個顏色的百分比,也讓佔比最高的主導色決定畫布的光暈。整個開瓶與色卡彈窗看起來有模有樣,底下放了一個寫著「珍藏這張月卡」的按鈕。

但如果你前幾天有因為好奇去按了那顆按鈕,會發現它就只是關閉視窗而已,沒有其他動作:

<button class="btn-seal" @click="showMonthlyCard = false">
  珍藏這張月卡
</button>

今天我們就要讓珍藏按鈕真正發揮作用,並蓋一間「時光相簿」,把過去每個月總結出來的色卡整整齊齊收納起來。

一、做相簿前的考量:即時重算 vs 封存快照?

在動手寫程式之前,我停下來想了一下資料要怎麼存。在 Vue 裡做這個功能,通常有兩種完全不同的思路:

  • 做法 A(即時運算):畫廊只存一份「月份清單」(例如 ['2026-09', '2026-10'])。使用者點進去哪個月,系統就當場把那個月的所有光珠抓出來,重新跑一次百分比和漸層計算。
  • 做法 B(色卡儲存):按下「珍藏」的那一秒,就把當下算好的色彩比例、總顆數、主導色直接打包成一筆獨立的紀錄存進 localStorage。電子相簿只負責讀這張「已經完整的卡片」。

本來想偷懶選 A,反正歷史光珠都在,隨叫隨算好像很省事。但轉念一想:

  • 既然月度色卡在封存當下就已經完成統計,就沒有必要在每次查看歷史卡片時重新計算;直接保存計算結果,也能讓歷史卡片保留當時的狀態。

所以我選擇了做法 B 作為之後要呈現的方向。

二、實作:按鈕接上封存邏輯

既然決定做整張色卡的儲存,資料結構就不用太複雜,只要記錄月份、總顆數、佔比陣列與主導色就好:

// 初始化已珍藏的月度色卡清單
const archivedCards = ref(JSON.parse(localStorage.getItem('store_archived_cards') || '[]'))

// 點擊「珍藏這張月卡」時執行
function sealMonthlyCard() {
  const monthTarget = cardMonthTarget.value
  if (!monthTarget || colorStats.value.length === 0) {
    showMonthlyCard.value = false
    return
  }

  // 檢查相簿裡是不是已經存過這個月份了
  const existingIndex = archivedCards.value.findIndex(card => card.month === monthTarget)
  
  const cardSnapshot = {
    month: monthTarget,
    totalGems: targetMonthGems.value.length,
    stats: colorStats.value,
    dominantColor: colorStats.value[0]?.color || '#ffffff',
  }

  if (existingIndex !== -1) {
    // 如果當月有存過,就覆蓋更新
    archivedCards.value[existingIndex] = cardSnapshot
  } else {
    // 如果是第一次珍藏:放到陣列最前面,讓相簿最新月份排在最上面
    archivedCards.value.unshift(cardSnapshot)
  }

  // 存進 localStorage 並關閉彈窗
  localStorage.setItem('store_archived_cards', JSON.stringify(archivedCards.value))
  showMonthlyCard.value = false
}

接著把按鈕的點擊事件換掉:

<button class="btn-seal" @click="sealMonthlyCard">珍藏這張月卡</button>

現在點下去不再是關閉彈窗,而是會有一張月度卡片被寫進了儲存庫。

三、打造時光相簿

有了存檔,接下來需要一個入口跟展示空間。

1. 入口位置與動線整併

原本隨手把相簿按鈕丟在畫面最底下,實機測試時發現畫面被拉得很長,而且孤零零一顆按鈕顯得很突兀。
最後我決定把「時光相簿」收進瓶子下方的操作列,跟「查看本月微光色卡」並排放在一起,並直接在按鈕上顯示目前收藏的張數:

<!-- 瓶子下方的操作按鈕列 -->
<div class="card-action-row">
  <button class="btn-sub-capsule" @click="openCurrentMonthCard">
    查看本月微光色卡
  </button>
  <button class="btn-sub-capsule" @click="showAlbum = true">
    時光相簿({{ archivedCards.length }})
  </button>
</div>

2. 不能忽視的「空狀態」

剛開始用的使用者,相簿點進去一定什麼都沒有。如果只留下一片空白,看起來會很像系統壞掉。所以我給它補了一個空狀態的提示詞:

<!-- 時光相簿彈窗 -->
<div v-if="showAlbum" class="album-overlay" @click.self="showAlbum = false">
  <div class="album-modal">
    <div class="album-header">
      <div>
        <h2>時光相簿</h2>
        <p>收錄每個月珍藏的總結色卡</p>
      </div>
      <button class="btn-close-album" @click="showAlbum = false">✕</button>
    </div>

    <!-- 還沒有卡片時的空狀態 -->
    <div v-if="archivedCards.length === 0" class="album-empty">
      <p class="empty-text">相簿裡還空空的,沒有任何紀錄。</p>
      <p class="empty-hint">到了月底,在月度色卡點擊「珍藏這張月卡」就會收錄在這裡。</p>
    </div>

    <!-- 有卡片 -->
    <div v-else class="album-grid">
      <div 
        v-for="card in archivedCards" 
        :key="card.month" 
        class="album-card-item"
      >
        <!-- 迷你圖片 -->
        <div class="mini-aurora-frame" :style="getCardGradientStyle(card)">
          <div class="mini-inner-glow"></div>
          <span class="mini-month-tag">{{ card.month }}</span>
        </div>

        <!-- 卡片資訊摘要 -->
        <div class="mini-card-meta">
          <span class="mini-gem-count">{{ card.totalGems }} 顆光珠</span>
          <div class="mini-swatches">
            <span 
              v-for="(st, idx) in card.stats.slice(0, 3)" 
              :key="idx" 
              class="mini-swatch-dot" 
              :style="{ backgroundColor: st.color }"
              :title="`${st.ratio}%`"
            ></span>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

為了讓每張小圖卡能還原當時的色彩,這邊補上了一個計算微型漸層背景的輔助函式:

// 依據數據產生小圖卡的色彩背景
function getCardGradientStyle(card) {
  if (!card.stats || card.stats.length === 0) return { background: '#2d3748' }
  const anchorPoints = ['25% 25%', '80% 75%', '75% 25%', '20% 80%', '50% 50%']
  const radialLayers = card.stats.map((item, idx) => {
    const pos = anchorPoints[idx % anchorPoints.length]
    const renderColor = item.color.toLowerCase() === '#ffffff' ? 'rgba(255, 255, 255, 0.9)' : item.color
    const radius = Math.min(90, Math.max(50, item.ratio + 30))
    return `radial-gradient(circle at ${pos}, ${renderColor} 0%, rgba(255, 255, 255, 0) ${radius}%)`
  })
  return {
    backgroundImage: radialLayers.join(', '),
    backgroundColor: card.dominantColor || card.stats[0].color
  }
}

在相簿裡,每張卡片就是縮小版的色卡:上面是漸層(沿用色卡顏色佔比產生),左下角壓上月份標籤,下方則列出該月的總顆數和前三名的重點色票。

四、實作過程遇到的問題

在把彈窗組件拼裝進來時,有兩個小插曲發生:

  1. 按鈕點了沒反應
    一開始在 template 裡寫了 @click="showAlbum = true",實機按下去卻毫無動靜。打開 Console 檢查才發現自己漏了一步:<script setup> 裡忘了宣告 const showAlbum = ref(false)。補上狀態宣告並把彈窗專屬的遮罩樣式(position: fixed)加上去後,相簿才順利跑出來。
  2. 月份抓取的函式分工:
    原本按鈕接的是提供給隔月 1 號補領的 openPreviousMonthCard,導致明明是當月卻一直開成上個月的空卡片。將它改為獨立的 openCurrentMonthCard 鎖定當月後,色卡就能正確抓到當月的數據了。

今日成果與驗收

  1. 封存閉環建立:點擊「珍藏這張月卡」能將計算完成的狀態封存到 localStorage,外面按鈕數字隨之由 (0) 變為 (1)。

https://ithelp.ithome.com.tw/upload/images/20261005/20178067h54DR1tZgV.png

https://ithelp.ithome.com.tw/upload/images/20261005/20178067y1CVmrLD9h.png

  1. 相簿陳列完成:平時隨時可以從瓶身下方點開「時光相簿」,未收藏時看得到友善的空狀態提示;已收藏時則整齊呈現出過去每個月的迷你色卡。
    https://ithelp.ithome.com.tw/upload/images/20261005/20178067XQ2oYuAEnv.png

https://ithelp.ithome.com.tw/upload/images/20261005/20178067UrwY6VVVKg.png

明日預告

現在卡片已經能好好存在時光相簿裡了,但目前在相簿中,它們還只是一張張縮微的小圖片。

明天(Day 22),我們要實作「色卡匯出成圖片下載」與「相簿卡片點擊展開大圖」的功能!
我們將採用雙入口的設計:

  1. 當下隨手存:月底開瓶查看色卡時,就能直接一鍵下載圖檔分享。
  2. 隨時回憶存:之後從時光相簿點開任何一張歷史色卡展開大圖時,隨時都能再次下載留念。
    使用 Canvas 技術把專屬的總結色卡變成一張高畫質圖檔,讓這份記憶能存進手機相簿或分享到社群限動!

上一篇
【Day 20】拒絕千篇一律:顏色佔比演算法與色卡調整
下一篇
【Day 22】儲存美好回憶:微光色卡展開檢視與圖片下載
系列文
微光採集瓶:用 Vue 3 打造每日美好記錄網頁 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言